package server.java.autoduel.Player;

import java.awt.Point;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.logging.Logger;

import server.java.autoduel.Server;
import server.java.autoduel.Cars.Chassis;
import server.java.autoduel.Cars.ChassisFactory;
import server.java.autoduel.Locations.Location;
import server.java.autoduel.Network.IPacketizable;
import server.java.autoduel.Network.OPCODES;
import server.java.autoduel.Network.Util;
import server.java.autoduel.World.Grid;
import server.java.autoduel.World.World;

import com.sun.sgs.app.AppContext;
import com.sun.sgs.app.ClientSession;
import com.sun.sgs.app.ClientSessionListener;
import com.sun.sgs.app.DataManager;
import com.sun.sgs.app.ManagedObject;
import com.sun.sgs.app.ManagedReference;
import com.sun.sgs.app.NameNotBoundException;

/**
 * Represents a Player within autoduel
 * 
 * @author Ian Axelrod
 * @author Liz Ha
 * @author Andrew Muldowney
 * @author Edward Futch
 */
public class Player implements ManagedObject, ClientSessionListener,
		Serializable, IPacketizable {
	private static final long serialVersionUID = -5268352875114108364L;

	/** The {@link Logger} for this class. */
	private static final Logger logger = Logger.getLogger(Player.class
			.getName());

	/**
	 * The name of the player
	 */
	private String name;

	/**
	 * The player's mechanical skill
	 */
	private Integer mechanicSkill;

	/**
	 * The health of the player
	 */
	private Integer health;

	/**
	 * The current location of the player
	 */
	private Point currentCoord = new Point(512 + 512 + 256, 288 + 288 + 144);

	private String image;
	
	/**
	 * Marks whether or not this player is 
	 * in a location
	 */
	private boolean inLocation;

	/**
	 * @return the inLocation
	 */
	public boolean isInLocation() {
		return inLocation;
	}

	/**
	 * @param inLocation the inLocation to set
	 */
	public void setInLocation(boolean inLocation) {
		this.inLocation = inLocation;
	}

	/**
	 * A reference to the gameworld the player resides in
	 */
	private ManagedReference<World> worldRef;

	/** The message encoding. */
	protected static final String PLAYER_BIND_PREFIX = "Duelist: ";

	/**
	 * Parse a packet to a player object
	 * 
	 * @param packet
	 *            the packet to parse
	 * @return the resultant player object
	 */
	public static Player decodePacket(ByteBuffer packet, ClientSession session,
			OPCODES opcode) {
		final Player player = new Player(Util.makeString(packet), session);

		switch (opcode) {
		case ORD_UPDPLAYER: {
			player.setMechanicSkill(Integer.reverseBytes(packet.getInt()));
			player.setHealth(Integer.reverseBytes(packet.getInt()));
			player.setCurrentCoord(new Point(Integer.reverseBytes(packet
					.getInt()), Integer.reverseBytes(packet.getInt())));
			player.setInLocation(packet.get()==1);
		}
		}
		return player;
	}

	/**
	 * Signifies the player has logged in
	 * 
	 * @param session
	 *            client session
	 * @return signed in player instance
	 */
	public static Player loggedIn(ClientSession session) {
		final String playerName = Player.PLAYER_BIND_PREFIX + session.getName();

		// try to find player object, if non existent then create
		final DataManager dataMgr = AppContext.getDataManager();
		Player player = null;

		try {
			// found saved player, restoring his session
			player = (Player) dataMgr.getBinding(playerName);
			player.setSession(session);
			Player.logger.info("Duelest logged in: " + player.getName());
		} catch (final NameNotBoundException ex) {
			// this is a new player
			player = PlayerFactory.providePlayer(playerName, session);
			Player.logger.info("New duelist created: " + player.getName());
			dataMgr.setBinding(playerName, player);
		}
		return player;
	}

	/**
	 * Our current session
	 */
	private ManagedReference<ClientSession> sessionRef;

	/**
	 * Internal use variable meant to let helper methods know if the player
	 * coordinates need to be sent to the client again
	 */
	private boolean reevaluateCoord;

	/**
	 * Construct a player
	 * 
	 * @param name
	 *            the player's name
	 * @param session
	 *            the client session for the player
	 */
	public Player(String name, ClientSession session) {
		Player.logger.setLevel(Server.loggerLevel);
		final DataManager dataManager = AppContext.getDataManager();

		dataManager.markForUpdate(this);
		this.name = name;

		this.sessionRef = dataManager.createReference(session);
	}

	/**
	 * Construct a player
	 * 
	 * @param name
	 *            the player's name
	 * @param mechanicSkill
	 *            the player's mechanic skill
	 * @param health
	 *            the player's health
	 * @param image
	 *            the path to the sprite used for the player
	 */
	public Player(String name, Integer mechanicSkill, Integer health,
			String image, ClientSession session) {
		Player.logger.setLevel(Server.loggerLevel);
		final DataManager dataManager = AppContext.getDataManager();

		dataManager.markForUpdate(this);

		this.name = name;
		this.mechanicSkill = mechanicSkill;
		this.health = health;
		this.image = image;

		this.sessionRef = dataManager.createReference(session);
	}

	/**
	 * Sends a message to all grids around & including a target grid telling
	 * them to remove a player
	 * 
	 * @param target
	 *            the target grid
	 * @param player
	 *            the player to remove
	 */
	public void broadcastRemovePlayerAround(Grid target, Player player) {
		final Grid[] adjGrids = this.worldRef.get().getAdjacentGrids(target);

		target.sendMessageExempting(player.encodePacket(OPCODES.ORD_REMPLAYER),
				player);
		for (final Grid adjGrid : adjGrids) {
			adjGrid.sendMessageExempting(
					player.encodePacket(OPCODES.ORD_REMPLAYER), player);
		}
	}

	/**
	 * Sends a message to necessary grids around & including a target grid to
	 * tell those grids to update their information
	 * 
	 * @param target
	 *            the target grid to update around
	 * @param old
	 *            the old grid to see how we need to shift
	 */
	public void broadcastShiftUpdateGridsAround(Grid target, Grid old) {
		final Grid[] adjGrids = this.worldRef.get().getAdjacentGrids(target);
		// final Grid[] oldAdjGrids = this.worldRef.get().getAdjacentGrids(old);

		// update current session's grids
		target.setIsPlayerGrid((byte) 1);

		sendMessage(target.encodePacket(OPCODES.ORD_UPDGRID));
		for (final Location location : target.getLocations()) {
			sendMessage(location.encodePacket(OPCODES.ORD_UPDLOC));
		}

		for (final Grid adjGrid : adjGrids) {
			adjGrid.setIsPlayerGrid((byte) 0);

			sendMessage(adjGrid.encodePacket(OPCODES.ORD_UPDGRID));
			for (final Location location : adjGrid.getLocations()) {
				sendMessage(location.encodePacket(OPCODES.ORD_UPDLOC));
			}
		}
	}

	/**
	 * Sends a message to all grids around & including a target grid to tell
	 * those grids to update their information
	 * 
	 * @param target
	 *            the target grid to update around
	 */
	public void broadcastUpdateGridsAround(Grid target) {
		final Grid[] adjGrids = this.worldRef.get().getAdjacentGrids(target);

		// update current session's grids
		target.setIsPlayerGrid((byte) 1);
		target.setLocalIndex(new Point(1, 1));

		sendMessage(target.encodePacket(OPCODES.ORD_UPDGRID));
		for (final Location location : target.getLocations()) {
			sendMessage(location.encodePacket(OPCODES.ORD_UPDLOC));
		}

		for (final Grid adjGrid : adjGrids) {
			adjGrid.setIsPlayerGrid((byte) 0);
			sendMessage(adjGrid.encodePacket(OPCODES.ORD_UPDGRID));
			/*for (final Location location : adjGrid.getLocations()) {
				sendMessage(location.encodePacket(OPCODES.ORD_UPDLOC));
			}*/
		}
	}

	/**
	 * Sends a message to all grids around & including a target grid telling
	 * them to update their view of a player
	 * 
	 * @param target
	 *            the target player
	 * @param player
	 *            the player the grids are being updated with
	 */
	public void broadcastUpdatePlayersAround(Grid target, Player player) {
		final Grid[] adjGrids = this.worldRef.get().getAdjacentGrids(target);

		// Done in case we need a coordinate reevaluation
		if (this.reevaluateCoord) {
			sendMessage(player.encodePacket(OPCODES.ORD_UPDPLAYER));
			this.reevaluateCoord = false;
		}

		// notify all players in all relevant grids of the new player's addition
		// to the world
		target.sendMessageExempting(player.encodePacket(OPCODES.ORD_UPDPLAYER),
				player);
		for (final Grid adjGrid : adjGrids) {
			adjGrid.sendMessageExempting(
					player.encodePacket(OPCODES.ORD_UPDPLAYER), player);
		}
	}

	/**
	 * Sends a message to all players in view of the target player telling them
	 * to update their information about that player
	 * 
	 * @param target
	 *            the target player
	 */
	public void broadcastUpdatePlayersAround(Player target) {
		final Player[] players = this.worldRef.get().getPlayersInView(target);

		// Done in case we need a coordinate reevaluation
		sendMessage(target.encodePacket(OPCODES.ORD_UPDPLAYER));

		// update current session with all players in its view
		for (final Player player : players) {
			sendMessage(player.encodePacket(OPCODES.ORD_UPDPLAYER));
		}
	}

	/**
	 * Create a copy of the player with a new name & client session
	 * 
	 * @param name
	 *            the new name for the player
	 * @param session
	 *            the new client session for the player
	 * @return a copy of the player
	 */
	public Player copy(String name, ClientSession session) {
		return new Player(name, this.mechanicSkill, this.health, this.image,
				session);
	}

	@Override
	public void disconnected(boolean graceful) {
		Player.logger.info("Player disconnected: " + this.name);

		final World world = this.worldRef.getForUpdate();

		// get him out of the world
		world.removePlayer(this);

		// Tell other players he's gone.
		final Grid grid = world.getGridFromCoord(this.currentCoord);
		final Grid[] adjGrids = world.getAdjacentGrids(grid);

		grid.sendMessage(encodePacket(OPCODES.ORD_REMPLAYER));

		for (final Grid adjGrid : adjGrids) {
			adjGrid.sendMessage(encodePacket(OPCODES.ORD_REMPLAYER));
		}
	}

	@Override
	public ByteBuffer encodePacket(OPCODES opcode) {
		switch (opcode) {
		case ORD_UPDPLAYER: {
			final byte[] bytes = new byte[1 + // opcode
					this.name.length() + 1 + // delim
					4 + // mechSkill
					4 + // health
					4 + // currentCoordx
					4 + // currentCoordy
					1 + // inLocation
					1]; // delim
			final ByteBuffer buf = ByteBuffer.wrap(bytes);

			buf.put((byte) OPCODES.ORD_UPDPLAYER.ordinal());
			buf.put(this.name.getBytes());
			buf.put(Util.PKT_DLM);
			buf.putInt(this.mechanicSkill);
			buf.putInt(this.health);
			buf.putInt(this.currentCoord.x);
			buf.putInt(this.currentCoord.y);
			buf.put((this.inLocation) ? (byte)1 : (byte)0);
			buf.put(Util.PKT_DLM);
			buf.flip();

			return buf;
		}
		case ORD_REMPLAYER: {
			final byte[] bytes = new byte[1 + // opcode
			this.name.length() + 1]; // delim
			final ByteBuffer buf = ByteBuffer.wrap(bytes);

			buf.put((byte) OPCODES.ORD_REMPLAYER.ordinal());
			buf.put(this.name.getBytes());
			buf.put(Util.PKT_DLM);
			buf.flip();

			return buf;
		}
		case ORD_ADMIN: {
			final byte[] bytes = new byte[1 + // opcode
			1]; // delim
			final ByteBuffer buf = ByteBuffer.wrap(bytes);
			buf.put((byte) OPCODES.ORD_ADMIN.ordinal());
			buf.put(Util.PKT_DLM);
			buf.flip();

			return buf;
		}
		case ORD_UPDPLAYERLIST: {
			final byte[] bytes = new byte[1 + // opcode
			this.name.length() + 1]; // delim
			final ByteBuffer buf = ByteBuffer.wrap(bytes);

			buf.put((byte) OPCODES.ORD_UPDPLAYERLIST.ordinal());
			buf.put(this.name.getBytes());
			buf.put(Util.PKT_DLM);
			buf.flip();

			return buf;
		}
		case ORD_ENDUPDPLAYERLIST: {
			final byte[] bytes = new byte[1 + // opcode
			1]; // delim
			final ByteBuffer buf = ByteBuffer.wrap(bytes);

			buf.put((byte) OPCODES.ORD_ENDUPDPLAYERLIST.ordinal());
			buf.put(Util.PKT_DLM);
			buf.flip();

			return buf;
		}
		default:
			return null;
		}
	}

	@Override
	public boolean equals(Object obj) {
		if ((obj != null) && (obj instanceof Player)) {
			return this.name.equals(((Player) obj).getName());
		} else {
			return false;
		}
	}

	/**
	 * @return the currentCoord
	 */
	public Point getCurrentCoord() {
		return this.currentCoord;
	}

	/**
	 * @return the health
	 */
	public Integer getHealth() {
		return this.health;
	}

	/**
	 * @return the mechanicSkill
	 */
	public Integer getMechanicSkill() {
		return this.mechanicSkill;
	}

	/**
	 * @return the name
	 */
	public String getName() {
		return this.name;
	}

	/**
	 * Gets the client session for this player
	 * 
	 * @return the client session for this player
	 */
	public ClientSession getSession() {
		if (this.sessionRef != null) {
			return this.sessionRef.get();
		} else {
			return null;
		}
	}

	/**
	 * Handles a player joining the world
	 * 
	 * @param world
	 *            the world this player is joining
	 */
	public void join(World world) {
		world.addPlayer(this);
		setWorldRef(world);
		Player.logger.info("Player " + this.name
				+ " joined the autoduel world.");
	}

	private void kickPlayer(Player player) {
		final DataManager dataManager = AppContext.getDataManager();

		dataManager.markForUpdate(player);
		dataManager.removeObject(player.getSession());
		player.sessionRef = null;
	}

	@Override
	public void receivedMessage(ByteBuffer message) {
		final byte opcode = message.get();

		if (opcode == (byte) OPCODES.REQ_UPDWORLD.ordinal()) {
			Player.logger.info("Client: " + this.name
					+ " requested a world update.");

			final Grid grid = this.worldRef.get().getGridFromCoord(
					getCurrentCoord());

			// update local world area
			broadcastUpdateGridsAround(grid);
			broadcastUpdatePlayersAround(grid, this);
			broadcastUpdatePlayersAround(this);

			// update local world chasses list
			updateChassesList();
		} else if (opcode == (byte) OPCODES.REQ_UPDPLAYER.ordinal()) {
			Player.logger.info("Client: " + this.name
					+ " requested a player update.");
			sendMessage(encodePacket(OPCODES.ORD_UPDPLAYER));
		} else if (opcode == (byte) OPCODES.ORD_UPDPLAYER.ordinal()) {

			Player.logger.info("Client: " + this.name
					+ " ordered server to update the player model.");
			final World world = this.worldRef.get();

			final Grid oldGrid = world.getGridFromCoord(this.currentCoord);

			final Player player = Player.decodePacket(message,
					this.sessionRef.get(), OPCODES.ORD_UPDPLAYER);

			AppContext.getDataManager().markForUpdate(this);

			this.name = player.getName();
			this.mechanicSkill = player.getMechanicSkill();
			this.health = player.getHealth();
			this.currentCoord = player.getCurrentCoord();
			this.inLocation = player.isInLocation();

			Player.logger.info("Player updated to: " + toString());

			// Check the special cases where we are at the edge of the world
			/*
			 * if ((this.currentCoord.x / Grid.Width) >= World.GRIDS_X) {
			 * this.currentCoord.x = 0; //this.reevaluateCoord = true; } if
			 * ((this.currentCoord.y / Grid.Height) >= World.GRIDS_Y) {
			 * this.currentCoord.y = 0; //this.reevaluateCoord = true; } if
			 * (this.currentCoord.y < 0) { this.currentCoord.y = Grid.Height *
			 * (World.GRIDS_Y); //this.reevaluateCoord = true; } if
			 * (this.currentCoord.x < 0) { this.currentCoord.x = Grid.Width *
			 * (World.GRIDS_X); //this.reevaluateCoord = true; }
			 */
			// notify all players in all relevant grids of the change in the
			// player

			final Grid newGrid = world.getGridFromCoord(getCurrentCoord());
			Player.logger
					.info("---------------------------------OldCoords: "
							+ oldGrid.getCoord() + " New Coords: "
							+ newGrid.getCoord());
			// in the case the player is in a new grid, we must repopulate the
			// player's world
			// entirely on the local end
			if (oldGrid.getCoord() != newGrid.getCoord()) {
				oldGrid.removePlayer(this);
				newGrid.addPlayer(this);
				// TODO: Do this optimization if necessary
				broadcastShiftUpdateGridsAround(newGrid, oldGrid);
			}

			Player.logger.info("*******************Players Grid: "
					+ newGrid.toString() + "******************************");
			// broadcastRemovePlayerAround(oldGrid, this);
			broadcastUpdatePlayersAround(newGrid, this);
		} else if (opcode == (byte) OPCODES.REQ_ADMIN.ordinal()) {
			sendMessage(encodePacket(OPCODES.ORD_ADMIN));
		} else if (opcode == (byte) OPCODES.ORD_REMPLAYER.ordinal()) {
			final Player player = this.worldRef.get().getPlayerByName(
					Player.decodePacket(message, this.sessionRef.get(),
							OPCODES.ORD_REMPLAYER).getName());

			if (player != null) {
				kickPlayer(player);
			}
		} else if (opcode == (byte) OPCODES.ORD_BANPLAYER.ordinal()) {
			final Player player = (Player) AppContext.getDataManager()
					.getBinding(
							Player.decodePacket(message, this.sessionRef.get(),
									OPCODES.ORD_BANPLAYER).getName());

			if (player != null) {
				// if the player is logged in, we kick him
				if (this.worldRef.get().getPlayerByName(player.getName()) != null) {
					kickPlayer(player);
				}

				// remove the player from the database
				AppContext.getDataManager().removeBinding(player.getName());
			}
		} else if (opcode == (byte) OPCODES.REQ_UPDPLAYERLIST.ordinal()) {
			Player.logger.info("Client: " + this.name
					+ " requested a server database player list update.");
			final DataManager dataManager = AppContext.getDataManager();
			String boundName = dataManager.nextBoundName(null);

			while (boundName != null) {
				final Player player = (Player) dataManager
						.getBinding(boundName);

				sendMessage(player.encodePacket(OPCODES.ORD_UPDPLAYERLIST));
				boundName = dataManager.nextBoundName(boundName);
			}
			sendMessage(encodePacket(OPCODES.ORD_ENDUPDPLAYERLIST));
		} else {
			Player.logger.info("Client: " + this.name
					+ " request could not be determined.");
		}
	}

	/**
	 * Send a message to the client
	 * 
	 * @param message
	 *            the message to send
	 */
	public void sendMessage(ByteBuffer message) {
		this.sessionRef.get().send(message);
	}

	/**
	 * @param currentCoord
	 *            the currentCoord to set
	 */
	public void setCurrentCoord(Point currentCoord) {
		AppContext.getDataManager().markForUpdate(this);
		this.currentCoord = currentCoord;
	}

	/**
	 * @param health
	 *            the health to set
	 */
	public void setHealth(Integer health) {
		AppContext.getDataManager().markForUpdate(this);
		this.health = health;
	}

	/**
	 * @param mechanicSkill
	 *            the mechanicSkill to set
	 */
	public void setMechanicSkill(Integer mechanicSkill) {
		AppContext.getDataManager().markForUpdate(this);
		this.mechanicSkill = mechanicSkill;
	}

	/**
	 * @param name
	 *            the name to set
	 */
	public void setName(String name) {
		AppContext.getDataManager().markForUpdate(this);
		this.name = name;
	}

	/**
	 * Sets the client session for this player
	 * 
	 * @param session
	 *            the session to set
	 */
	public void setSession(ClientSession session) {
		final DataManager dataManager = AppContext.getDataManager();

		dataManager.markForUpdate(this);

		this.sessionRef = dataManager.createReference(session);
	}

	/**
	 * Sets the world the player is in
	 * 
	 * @param world
	 *            the world the player is in
	 */
	protected void setWorldRef(World world) {
		final DataManager dataManager = AppContext.getDataManager();
		dataManager.markForUpdate(this);

		this.worldRef = dataManager.createReference(world);
	}

	@Override
	public String toString() {
		return "name: " + this.name + " -- mechanicSkill: "
				+ this.mechanicSkill + " -- health: " + this.health
				+ " -- image: " + this.image + " -- coords: "
				+ this.currentCoord + " -- inLocation?: "
				+ this.inLocation;
	}

	/**
	 * Update the client's list of available chasses
	 */
	private void updateChassesList() {

		// update the player with all available chasses
		final ArrayList<Chassis> chasses = ChassisFactory.provideChasses();

		for (final Chassis chassis : chasses) {
			sendMessage(chassis.encodePacket(OPCODES.ORD_UPDCHASSESLIST));
		}
	}
}